//
// SVN Notifier
// Copyright 2007 SIA Computer Hardware Design (www.chd.lv)
//
// This file is part of SVN Notifier.
//
// SVN Notifier is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// SVN Notifier is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>
//

using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Windows.Forms;

namespace CHD.SVN_Notifier
{
	public enum SvnFolderStatus
	{
		Unknown,
		Error,
		NeedUpdate,
		NeedUpdate_Modified,
		UpToDate,
		UpToDate_Modified
	}


	public class SvnFolderProcess
	{
		public readonly SvnFolder folder;
		public readonly Process process;
		public readonly bool isUpdateCommand;
		public ArrayList processOutput = new ArrayList ();


		public SvnFolderProcess (SvnFolder f, Process p, bool isUpdateCommand)
		{
			folder = f;
			process = p;
			this.isUpdateCommand = isUpdateCommand;
		}
	}


	public delegate void SvnErrorAddedHandler (string path, string error);


	public class SvnTools
	{
		public static event SvnErrorAddedHandler ErrorAdded;

		// TortoiseSVN switches
		private const string tortoiseChangeLogArguments = "/command:log /path:\"{0}\" /revend:{1}";
		private const string tortoiseLogArguments = "/command:log /path:\"{0}\"";
		private const string tortoiseUpdateArguments = "/command:update /path:\"{0}\"{1} /notempfile /closeonend:2";
		private const string tortoiseCommitArguments = "/command:commit /path:\"{0}\" /notempfile";

		// SVN switches
		private const string svnStatusArguments = "status -u --non-interactive \"{0}\"";
		private const string svnInfoArguments = "info --non-interactive -r {0} \"{1}\"";
		private const string svnUpdateArguments = "update --non-interactive {0}";

		public static ArrayList svnFolderProcesses = ArrayList.Synchronized (new ArrayList());
		private static Process backgroundProcess;


		public static int GetLastChangeRev (string path, string arg)
		{
			string arguments = String.Format (svnInfoArguments, arg, path);
			ExecuteResult er = ExecuteProcess (Config.SVNpath, path, arguments, true, false);

			int maxLastChangedRev = 0;
			foreach (string line in er.processOutput.Split ('\n'))
				if (line.IndexOf ("Last Changed Rev: ") != -1)
				{
					int lastChangedRev = Convert.ToInt32 (line.Replace ("Last Changed Rev: ", ""));
					if (lastChangedRev > maxLastChangedRev)
						maxLastChangedRev = lastChangedRev;
				}
			return maxLastChangedRev;
		}

		
		public static void OpenChangeLogWindow (SvnFolder folder)
		{
			folder.headRevision = GetLastChangeRev (folder.Path, "HEAD");
			folder.workRevision = GetLastChangeRev (folder.Path, "COMMITTED");
			string arguments = String.Format (tortoiseChangeLogArguments, folder.Path, folder.workRevision + 1);
			ExecuteProcess (Config.TortoiseSVNpath, null, arguments, false, false);
		}
		

		public static void OpenLogWindow (string path)
		{
			string arguments = String.Format (tortoiseLogArguments, path);
			ExecuteProcess (Config.TortoiseSVNpath, null, arguments, false, false);
		}


		/// <summary>
		/// This method waits until updating will finish
		/// </summary>
		public static void Update (SvnFolder folder)
		{
			string revision = Config.ChangeLogBeforeUpdate ? " /rev:" + folder.headRevision.ToString() : "";
			string arguments = String.Format (tortoiseUpdateArguments, folder.Path, revision);
			ExecuteProcess (Config.TortoiseSVNpath, null, arguments, true, false);
		}
		

		public static void BeginUpdateSilently (SvnFolder folder)
		{
			// Skip this folder if update or commit is in progress
			foreach (SvnFolderProcess sp in svnFolderProcesses)
				if (sp.folder.Path == folder.Path)
					return;

			string arguments = String.Format (svnUpdateArguments, folder.Path);
			ExecuteResult er = ExecuteProcess (Config.SVNpath, null, arguments, false, false);
			svnFolderProcesses.Add (new SvnFolderProcess (folder, er.process, true));
		}


		public static bool HasUpdateErrors (SvnFolderProcess cp)
		{
			bool result = false;
			string line;

			// Read output stream
			while (( line = cp.process.StandardOutput.ReadLine() ) != null)
			{
				cp.processOutput.Add (line);

				if ((line.Length > 1) && (line.StartsWith ("C ") || line.StartsWith ("svn")))
					result = true;
			}
			return result;
		}


		public static void Commit (SvnFolder folder)
		{
			string arguments = String.Format (tortoiseCommitArguments, folder.Path);
			ExecuteResult er = ExecuteProcess (Config.TortoiseSVNpath, null, arguments, false, false);
			svnFolderProcesses.Add (new SvnFolderProcess (folder, er.process, false));
		}


		public static SvnFolderStatus GetSvnFolderStatus (SvnFolder folder)
		{
			string path = folder.Path;
			if (!Directory.Exists (path) && !File.Exists (path))
				return SvnFolderStatus.Error;

			string arguments = String.Format (svnStatusArguments, path);
			ExecuteResult er = ExecuteProcess (Config.SVNpath, path, arguments, true, true);

			if (er.processError != "")
			{
				if (er.processError.IndexOf ("is not a working copy") != -1)	// TODO: Find better solution !!!
					return SvnFolderStatus.Error;
				else
					return SvnFolderStatus.Unknown;
			}
			
			bool modified = false;
			SvnFolderStatus status = SvnFolderStatus.UpToDate;

			foreach (string line in er.processOutput.Split ('\n'))
			{
				if (line.Length < 7) continue;

				if (line[7] == '*')								// TODO: Find better solution !!!
				{
					status = SvnFolderStatus.NeedUpdate;
					if ((line[0] != '?') && (line[0] != ' '))
						modified = true;
				}
				else if ((line[0] != '?') && (line[1] == ' '))
					modified = true;
			}

			if (modified)
				switch(status)
				{
					case SvnFolderStatus.UpToDate:
						return SvnFolderStatus.UpToDate_Modified;
					case SvnFolderStatus.NeedUpdate:
						return SvnFolderStatus.NeedUpdate_Modified;
				}
			
			return status;
		}


		private static ExecuteResult ExecuteProcess (string executionFile, string workingPath, string arguments, bool waitForExit, bool lowPriority)
		{
			ProcessStartInfo psi = new ProcessStartInfo();
			psi.FileName = executionFile;
			psi.Arguments = arguments;
			psi.CreateNoWindow = true;
			psi.UseShellExecute = false;
			psi.RedirectStandardOutput = true;
			psi.RedirectStandardError = true;

			ExecuteResult er = new ExecuteResult();
			er.process = Process.Start (psi);

			if (waitForExit) backgroundProcess = er.process;
			
			if (lowPriority)
			{
				try
				{
					er.process.PriorityClass = ProcessPriorityClass.Idle;
				}
				catch	// Exception may occur if process finishing or already finished
				{
				}
			}

			if (waitForExit)
			{
				ArrayList lines = new ArrayList();
				string line;

				// Read output stream
				while (( line = er.process.StandardOutput.ReadLine() ) != null)
					lines.Add (line);

				er.processOutput = String.Join ("\n", (string[]) lines.ToArray (typeof (string)));
				lines.Clear();

				// Read error stream
				while (( line = er.process.StandardError.ReadLine() ) != null)
					lines.Add (line);

				er.processError = String.Join ("\n", (string[]) lines.ToArray (typeof (string)));
				lines.Clear();

				if (er.processError.Length > 0)
				{
					if (ErrorAdded != null)
						ErrorAdded (workingPath, er.processError);
				}

				er.process.WaitForExit();

				if ((uint) er.process.ExitCode == 0xc0000142)		// STATUS_DLL_INIT_FAILED - Occurs when Windows shutdown in progress
				{
					Application.Exit();

					if (Thread.CurrentThread == MainForm.statusThread)
						Thread.CurrentThread.Abort();
				}

				backgroundProcess = null;
			}
			else
			{
				er.processOutput = "";
				er.processError = "";
			}

			return er;
		}
		
		
		public static void KillBackgroundProcess()
		{
			if (backgroundProcess != null)
			{
				try
				{
					backgroundProcess.Kill();
				}
				catch
				{
				}
			}
		}


		private struct ExecuteResult
		{
			public Process process;
			public string processError;
			public string processOutput;
		}
	}
}
